iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
Modern Web

關於我作夢變成工程師這檔事(Angular 篇)系列 第 8

第 8 天 邁出 RxJS 小小的一步|pipe、operators

  • 分享至 

  • xImage
  •  

前情提要

使用了 AsyncPipe 管道來取得所有英雄資料後,我們要在英雄資訊頁面,傳遞參數來取得特定的英雄資料。在先前撰寫的程式碼中,我們知道如何透過 ActivedRoute 提供的狀態快照 snapshot 來取得路由參數,再進一步透過 HttpClient 的 get 請求英雄資料。這看來是兩個步驟,但對於 RxJS 來說,它可以是一個資料流。今天,就讓我們玩起來吧!

但還是先聲明一下,這裡只會很扼要地說明語法完成了什麼事,和一些比較容易理解的觀念,想要深入地了解 RxJS 這邊 Mike 大大有本好書

重構取得個別英雄資料的方法

首先,我們先在 HeroDetailComponent 來重構 getHero() 方法,原本的程式碼如下:

export class HeroDetailComponent implements OnInit {

 hero: Hero | null = null;

  constructor(
    private http: HttpClient,
    private route: ActivatedRoute
  ) { }

  ngOnInit(): void {
    const heroId = this.route.snapshot.paramMap.get('id')!;
    this.getHero(heroId);
  }

  private getHero(id: string): void {
    this.http.get<Hero>(`api/heroes/${id}`).subscribe((selectedHero) => {
      this.hero = selectedHero;
    })
  }
}

我們調整成:

import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

import { Hero } from '../shared/models/hero.model';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {

  hero$: Observable<Hero>;

  constructor(
    private http: HttpClient,
    private route: ActivatedRoute
  ) {
    this.hero$ = this.getHero();
  }

  ngOnInit(): void {}

  private getHero(): Observable<Hero> {
    return this.route.paramMap.pipe(
      map((params) => params.get('id')),
      switchMap((heroId) => this.http.get<Hero>(`api/heroes/${heroId}`))
    );
  }

}

我們聚焦在 getHero() 方法完成的事情,也就是 hero$ 屬性到底會拿到什麼資料。當然我們知道,最後在 hero.detail.component.html 會使用 AsyncPipe 管道來訂閱資料並將資料顯示在畫面上:

<mat-card *ngIf="hero$| async as hero">
  <mat-card-header>
    <div mat-card-avatar></div>
    <mat-card-title>{{ hero.name }}</mat-card-title>
  </mat-card-header>
  (略)
  </mat-card-content>
</mat-card>

這個 hero 資料是經過種種過程才拿到畫面上的,而這個過程就是「資料流」。也就是說,當訂閱的時候,這道流就會往目的地流動;在流動過程中,可以對這些資料進行加工,比如,篩選資料(攻擊力低的英雄不要來啊、男英雄退散啊...)。好比水流經過了一個管道,在管道中進行了各種加工,最後可能變成可樂吧!

這個管道就是 pipe,而加工的方法就是操作符(operators),讓我們看看,在英雄資料流通過管道的時候,使用了哪些操作符進行加工:

  private getHero(): Observable<Hero> {
    return this.route.paramMap.pipe(
      map((params) => params.get('id')),
      switchMap((heroId) => this.http.get<Hero>(`api/heroes/${heroId}`))
    );
  }

map 的意圖就是轉換資料,在這裏我們將收到的 params,轉換成只取用 id (params.get('id')),並將轉換的資料繼續送往下個操作符 switchMap。

switchMap 會取消前一個訂閱,並發出新的可觀察者(observable),意思是,不會繼續將 heroId 往下傳遞,而是產生新的可觀察者(在這裏,發送了 http 請求來取得特定的英雄資料)。

雖然我們不會將 heroId 繼續往下傳遞,但可以使用它來做為發出 http 請求的參數。

這樣,就將原先拆成兩個步驟完成的功能,改寫成一個資料流。其他就同先前所做的,在畫面上使用 AsyncPipe 來訂閱這個資料。

程式碼更簡潔了,但好像哪裡怪怪的?

那股莫名的異樣感是什麼呢?

我們是重構了 getHero(),但取得資料的細節仍舊寫在 HeroDetailComponent 裡。不是說「與資料互動的事情交給 HeroService 服務就好了」嗎?

沒錯,接著我們會完成這件事。但先想一想:嗯?所以應該要在 HeroService 依賴注入 ActivedRoute 來取得路由參數嗎(heroId)?這樣就可以把 getHero() 整段程式搬到 HeroService 去了!太棒了!

是嗎?

或許不是這樣。誠然,取得特定英雄資料,必須有 heroId 作為參數。但是,取得 heroId 的方法可不是只能透過路由參數。記得嗎?一開始我們是透過屬性繫結的方法來取得英雄資料(包含了 heroId),也就是說,屬性繫結在某種情境下完全是可行的方案,當然,你可能還想得到其他取得方式。

作為 HeroService 服務,getHero() 方法要處理的事情是接受參數 heroId,並用它來發送 http 請求取得特定的英雄資料,至於 heroId 怎麼來,這個方法可能不需要負責。

所以 heroId 應該由注入此服務使用 getHero() 的地方來提供,也就是 HeroDetailComponent。

讓我們針對下列檔案進行調整:

hero.service.ts

  getHero(heroId: string): Observable<Hero> {
    return this.http.get<Hero>(`api/heroes/${heroId}`);
  }

而將 hero-detail.component.ts 的 getHero() 調整為:

  private getHero(): Observable<Hero> {
    return this.route.paramMap.pipe(
      map((params) => params.get('id')),
      switchMap((heroId) => this.heroService.getHero(heroId!))
    );
  }

也就是將 switchMap 操作符產生的新 observable,從原先的發送 http 請求,改為使用 HeroService 的 getHero()。這樣我們就完成了重構,程式正常運行:)

今天的程式碼已推上 Github


上一篇
第 7 天 讓元件歸元件、服務歸服務|service、@Injectable、AsyncPipe
下一篇
第 9 天 元件還是頁面,這是個問題|page、component
系列文
關於我作夢變成工程師這檔事(Angular 篇)14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言